/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.imagepipeline.memory;

import com.facebook.common.internal.Preconditions;
import com.facebook.common.internal.Sets;
import com.facebook.common.internal.Throwables;
import com.facebook.common.internal.VisibleForTesting;
import com.facebook.common.logging.FLog;
import com.facebook.common.memory.MemoryTrimType;
import com.facebook.common.memory.MemoryTrimmableRegistry;
import com.facebook.common.memory.Pool;
import com.facebook.imagepipeline.annotation.SuppressLint;
import ohos.utils.PlainArray;
import ohos.utils.PlainIntArray;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.NotThreadSafe;

/**
 * A base pool class that manages a pool of values (of type V).
 *
 * <p>The pool is organized as a map. Each entry in the map is a free-list (modeled by a queue) of
 * entries for a given size. Some pools have a fixed set of buckets (aka bucketized sizes), while
 * others don't.
 *
 * <p>The pool supports two main operations:
 *
 * <ul>
 *   <li>{@link #get(int)} - returns a value of size that's the same or larger than specified,
 *       hopefully from the pool; otherwise, this value is allocated (via the alloc function)
 *   <li>{@link #release(V)} - releases a value to the pool
 * </ul>
 * <p>
 * In addition, the pool subscribes to the {@link MemoryTrimmableRegistry}, and responds to
 * low-memory events (calls to trim). Some percent (perhaps all) of the values in the pool are then
 * released (via the underlying free function), and no longer belong to the pool.
 *
 * <p>Sizes There are 3 different notions of sizes we consider here (not all of them may be relevant
 * for each use case).
 *
 * <ul>
 *   <li>Logical size is simply the size of the value in terms appropriate for the value. For
 *       example, for byte arrays, the size is simply the length. For a bitmap, the size is just the
 *       number of pixels.
 *   <li>Bucketed size typically represents one of discrete set of logical sizes - such that each
 *       bucketed size can accommodate a range of logical sizes. For example, for byte arrays, using
 *       sizes that are powers of 2 for bucketed sizes allows these byte arrays to support a number
 *       of logical sizes.
 *   <li>Finally, Size-in-bytes is exactly that - the size of the value in bytes.
 * </ul>
 * <p>
 * Logical Size and BucketedSize are both represented by the type parameter S, while size-in-bytes
 * is represented by an int.
 *
 * <p>Each concrete subclass of the pool must implement the following methods
 *
 * <ul>
 *   <li>{@link #getBucketedSize(int)} - returns the bucketized size for the given request size
 *   <li>{@link #getBucketedSizeForValue(Object)} - returns the bucketized size for a given value
 *   <li>{@link #getSizeInBytes(int)} - gets the size in bytes for a given bucketized size
 *   <li>{@link #alloc(int)} - allocates a value of given size
 *   <li>{@link #free(Object)} - frees the value V Subclasses may optionally implement
 *   <li>{@link #onParamsChanged()} - called whenever this class determines to re-read the pool
 *       params
 *   <li>{@link #isReusable(Object)} - used to determine if a value can be reused or must be freed
 * </ul>
 *
 * <p>InUse values The pool keeps track of values currently in use (in addition to the free values
 * in the buckets). This is maintained in an IdentityHashSet (using reference equality for the
 * values). The in-use set helps with accounting/book-keeping; we also use this during {@link
 * #release(Object)} to avoid messing with (freeing/reusing) values that are 'unknown' to the pool.
 *
 * <p>PoolParams Pools are "configured" with a set of parameters (the PoolParams) supplied via a
 * provider. This set of parameters includes
 *
 * <ul>
 *   <li>{@link PoolParams#maxSizeSoftCap} The size of a pool includes its used and free space. The
 *       maxSize setting for a pool is a soft cap on the overall size of the pool. A key point is
 *       that {@link #get(int)} requests will not fail because the max size has been exceeded
 *       (unless the underlying {@link #alloc(int)} function fails). However, the pool's free
 *       portion will be trimmed as much as possible so that the pool's size may fall below the max
 *       size. Note that when the free portion has fallen to zero, the pool may still be larger than
 *       its maxSizeSoftCap. On a {@link #release(Object)} request, the value will be 'freed'
 *       instead of being added to the free portion of the pool, if the pool exceeds its
 *       maxSizeSoftCap. The invariant we want to maintain - see {@link #ensurePoolSizeInvariant()}
 *       - is that the pool must be below the max size soft cap OR the free lists must be empty.
 *   <li>{@link PoolParams#maxSizeHardCap} The hard cap is a stronger limit on the pool size. When
 *       this limit is reached, we first attempt to trim the pool. If the pool size is still over
 *       the hard, the {@link #get(int)} call will fail with a {@link PoolSizeViolationException}
 *   <li>{@link PoolParams#bucketSizes} The pool can be configured with a set of 'sizes' - a bucket
 *       is created for each such size. Additionally, each bucket can have a a max-length specified,
 *       which is the sum of the used and free items in that bucket. As with the MaxSize parameter
 *       above, the maxLength here is a soft cap, in that it will not cause an exception on get; it
 *       simply controls the release path. If the BucketSizes parameter is null, then the pool will
 *       dynamically create buckets on demand.
 * </ul>
 */
public abstract class BasePool<V> implements Pool<V> {
    private final Class<?> TAG = this.getClass();

    /**
     * The memory manager to register with
     */
    final MemoryTrimmableRegistry mMemoryTrimmableRegistry;

    /**
     * Provider for pool parameters
     */
    final PoolParams mPoolParams;

    /**
     * The buckets - representing different 'sizes'
     */
    @VisibleForTesting
    final PlainArray<Bucket<V>> mBuckets;

    /**
     * An Identity hash-set to keep track of values by reference equality
     */
    @VisibleForTesting
    final Set<V> mInUseValues;

    /**
     * Determines if new buckets can be created
     */
    private boolean mAllowNewBuckets;

    /**
     * tracks 'used space' - space allocated via the pool
     */
    @VisibleForTesting
    @GuardedBy("this")
    final Counter mUsed;

    /**
     * tracks 'free space' in the pool
     */
    @VisibleForTesting
    @GuardedBy("this")
    final Counter mFree;

    private final PoolStatsTracker mPoolStatsTracker;

    private boolean mIgnoreHardCap ;

    /**
     * Creates a new instance of the pool.
     *
     * @param poolParams       pool parameters
     * @param poolStatsTracker
     */
    public BasePool(
            MemoryTrimmableRegistry memoryTrimmableRegistry,
            PoolParams poolParams,
            PoolStatsTracker poolStatsTracker) {
        mMemoryTrimmableRegistry = Preconditions.checkNotNull(memoryTrimmableRegistry);
        mPoolParams = Preconditions.checkNotNull(poolParams);
        mPoolStatsTracker = Preconditions.checkNotNull(poolStatsTracker);

        // initialize the buckets
        mBuckets = new PlainArray<Bucket<V>>();
        if (mPoolParams.fixBucketsReinitialization) {
            initBuckets();
        } else {
            legacyInitBuckets(new PlainIntArray(1));
        }

        mInUseValues = Sets.newIdentityHashSet();

        mFree = new Counter();
        mUsed = new Counter();
    }

    public BasePool(
            MemoryTrimmableRegistry memoryTrimmableRegistry,
            PoolParams poolParams,
            PoolStatsTracker poolStatsTracker,
            boolean ignoreHardCap) {
        this(memoryTrimmableRegistry, poolParams, poolStatsTracker);
        mIgnoreHardCap = ignoreHardCap;
    }

    /**
     * Finish pool initialization.
     */
    protected void initialize() {
        mMemoryTrimmableRegistry.registerMemoryTrimmable(this);
        mPoolStatsTracker.setBasePool(this);
    }

    @Nullable
    protected synchronized V getValue(Bucket<V> bucket) {
        //noinspection deprecation
        return bucket.get();
    }

    /**
     * Gets a new 'value' from the pool, if available. Allocates a new value if necessary. If we need
     * to perform an allocation, - If the pool size exceeds the max-size soft cap, then we attempt to
     * trim the free portion of the pool. - If the pool size exceeds the max-size hard-cap (after
     * trimming), then we throw an {@link PoolSizeViolationException} Bucket length constraints are
     * not considered in this function
     *
     * @param size the logical size to allocate
     * @return a new value
     * @throws InvalidSizeException
     */
    public V get(int size) {
        //LogUtil.error(TAG.getSimpleName(),"--- get start ---");
        //LogUtil.error(TAG.getSimpleName(),"region release get A");
        ensurePoolSizeInvariant();

        int bucketedSize = getBucketedSize(size);
        int sizeInBytes = -1;
        //LogUtil.error(TAG.getSimpleName(),"get B");

        synchronized (this) {
            Bucket<V> bucket = getBucket(bucketedSize);
            //LogUtil.error(TAG.getSimpleName(),"get C");

            if (bucket != null) {
                //LogUtil.error(TAG.getSimpleName(),"get D");
                // find an existing value that we can reuse
                V value = getValue(bucket);
                //LogUtil.error(TAG.getSimpleName(),"get E");
                if (value != null) {
                    Preconditions.checkState(mInUseValues.add(value));
                    //LogUtil.error(TAG.getSimpleName(),"get F");

                    // It is possible that we got a 'larger' value than we asked for.
                    // lets recompute size in bytes here
                    bucketedSize = getBucketedSizeForValue(value);
                    //LogUtil.error(TAG.getSimpleName(),"get G");
                    sizeInBytes = getSizeInBytes(bucketedSize);
                    //LogUtil.error(TAG.getSimpleName(),"get H");
                    mUsed.increment(sizeInBytes);
                    //LogUtil.error(TAG.getSimpleName(),"get I");
                    mFree.decrement(sizeInBytes);
                    //LogUtil.error(TAG.getSimpleName(),"get J");
                    mPoolStatsTracker.onValueReuse(sizeInBytes);
                    //LogUtil.error(TAG.getSimpleName(),"get K");
                    logStats();
                    if (FLog.isLoggable(FLog.VERBOSE)) {
                        FLog.v(
                                TAG,
                                "get (reuse) (object, size) = (%x, %s)",
                                System.identityHashCode(value),
                                bucketedSize);
                    }
                    //LogUtil.error(TAG.getSimpleName(),String.format("region release  get (reuse) (object, size) = (%x, %s)", System.identityHashCode(value), bucketedSize));
                    return value;
                }
                // fall through
            }
            // check to see if we can allocate a value of the given size without exceeding the hard cap
            //LogUtil.error(TAG.getSimpleName(),"get L");
            sizeInBytes = getSizeInBytes(bucketedSize);
            //LogUtil.error(TAG.getSimpleName(),"get M");
            if (!canAllocate(sizeInBytes)) {
                //LogUtil.error(TAG.getSimpleName(),"get M1");
                throw new PoolSizeViolationException(
                        mPoolParams.maxSizeHardCap, mUsed.mNumBytes, mFree.mNumBytes, sizeInBytes);
            }

            // Optimistically assume that allocation succeeds - if it fails, we need to undo those changes
            //LogUtil.error(TAG.getSimpleName(),"get N");
            mUsed.increment(sizeInBytes);
            //LogUtil.error(TAG.getSimpleName(),"get O");
            if (bucket != null) {
                bucket.incrementInUseCount();
                //LogUtil.error(TAG.getSimpleName(),"get P");
            }
        }

        V value = null;
        try {
            // allocate the value outside the synchronized block, because it can be pretty expensive
            // we could have done the allocation inside the synchronized block,
            // but that would have blocked out other operations on the pool
            //LogUtil.error(TAG.getSimpleName(),"get R");
            value = alloc(bucketedSize);
            //LogUtil.error(TAG.getSimpleName(),"get S");
        } catch (Throwable e) {
            // Assumption we made previously is not valid - allocation failed. We need to fix internal
            // counters.
            synchronized (this) {
                //LogUtil.error(TAG.getSimpleName(),"get T");
                mUsed.decrement(sizeInBytes);
                //LogUtil.error(TAG.getSimpleName(),"get U");
                Bucket<V> bucket = getBucket(bucketedSize);
                //LogUtil.error(TAG.getSimpleName(),"get V");
                if (bucket != null) {
                    //LogUtil.error(TAG.getSimpleName(),"get W");
                    bucket.decrementInUseCount();
                    //LogUtil.error(TAG.getSimpleName(),"get X");
                }
            }
            Throwables.propagateIfPossible(e);
        }

        // NOTE: We checked for hard caps earlier, and then did the alloc above. Now we need to
        // update state - but it is possible that a concurrent thread did a similar operation - with
        // the result being that we're now over the hard cap.
        // We are willing to live with that situation - especially since the trim call below should
        // be able to trim back memory usage.
        synchronized (this) {
            Preconditions.checkState(mInUseValues.add(value));
            // If we're over the pool's max size, try to trim the pool appropriately
            //LogUtil.error(TAG.getSimpleName(),"get Y");
            trimToSoftCap();
            //LogUtil.error(TAG.getSimpleName(),"get Z");
            mPoolStatsTracker.onAlloc(sizeInBytes);
            //LogUtil.error(TAG.getSimpleName(),"get Z1");
            logStats();
            if (FLog.isLoggable(FLog.VERBOSE)) {
                FLog.v(
                        TAG,
                        "get (alloc) (object, size) = (%x, %s)",
                        System.identityHashCode(value),
                        bucketedSize);
            }
            /*
            LogUtil.error(TAG.getSimpleName(),String.format("region release get (alloc) (object, size) = (%x, %s)",
                    System.identityHashCode(value),
                    bucketedSize));

             */
        }

        //LogUtil.error(TAG.getSimpleName(),"--- get end ---");
        return value;
    }

    /**
     * Releases the given value to the pool. In a few cases, the value is 'freed' instead of being
     * released to the pool. If - the pool currently exceeds its max size OR - if the value does not
     * map to a bucket that's currently maintained by the pool, OR - if the bucket for the value
     * exceeds its maxLength, OR - if the value is not recognized by the pool then, the value is
     * 'freed'.
     *
     * @param value the value to release to the pool
     */
    @Override
    public void release(V value) {
        //LogUtil.error(TAG.getSimpleName(),"-----------------release start -----------------");
        //LogUtil.error(TAG.getSimpleName(),String.format("region release release this.hash:%x  value.hash: %x  %s",System.identityHashCode(this),System.identityHashCode(value),value));
        Preconditions.checkNotNull(value);

        final int bucketedSize = getBucketedSizeForValue(value);
        final int sizeInBytes = getSizeInBytes(bucketedSize);
        synchronized (this) {
            final Bucket<V> bucket = getBucketIfPresent(bucketedSize);
            if (!mInUseValues.remove(value)) {
                // This value was not 'known' to the pool (i.e.) allocated via the pool.
                // Something is going wrong, so let's free the value and report soft error.
                /*
                LogUtil.error(TAG.getSimpleName(),String.format("region release release 0 --> B free, value unrecognized) (object, size) = (%x, %s)",
                        System.identityHashCode(value),
                        bucketedSize));

                 */
                FLog.e(
                        TAG,
                        "region release release (free, value unrecognized) (object, size) = (%x, %s)",
                        System.identityHashCode(value),
                        bucketedSize);
                free(value);
                mPoolStatsTracker.onFree(sizeInBytes);
            } else {
                // free the value, if
                //  - pool exceeds maxSize
                //  - there is no bucket for this value
                //  - there is a bucket for this value, but it has exceeded its maxLength
                //  - the value is not reusable
                // If no bucket was found for the value, simply free it
                // We should free the value if no bucket is found, or if the bucket length cap is exceeded.
                // However, if the pool max size softcap is exceeded, it may not always be best to free
                // *this* value.
                if (bucket == null
                        || bucket.isMaxLengthExceeded()
                        || isMaxSizeSoftCapExceeded()
                        || !isReusable(value)) {
                    if (bucket != null) {
                        bucket.decrementInUseCount();
                    }

                    if (FLog.isLoggable(FLog.VERBOSE)) {
                        FLog.v(
                                TAG,
                                "region release (free) (object, size) = (%x, %s)",
                                System.identityHashCode(value),
                                bucketedSize);
                    }
                    /*
                    LogUtil.error(TAG.getSimpleName(),String.format("region release (free) (object, size) = (%x, %s)",
                            System.identityHashCode(value),
                            bucketedSize));
                     */
                    free(value);
                    mUsed.decrement(sizeInBytes);
                    mPoolStatsTracker.onFree(sizeInBytes);
                } else {
                    bucket.release(value);
                    mFree.increment(sizeInBytes);
                    mUsed.decrement(sizeInBytes);
                    mPoolStatsTracker.onValueRelease(sizeInBytes);
                    if (FLog.isLoggable(FLog.VERBOSE)) {
                        FLog.v(
                                TAG,
                                "region release (reuse) (object, size) = (%x, %s)",
                                System.identityHashCode(value),
                                bucketedSize);
                    }
                    /*
                    LogUtil.error(TAG.getSimpleName(),String.format("region release (reuse) (object, size) = (%x, %s)",
                            System.identityHashCode(value),
                            bucketedSize));
                     */
                }
            }
            logStats();
        }

        //LogUtil.error(TAG.getSimpleName(),"-----------------release end -----------------");
    }

    /**
     * Trims the pool in response to low-memory states (invoked from MemoryManager) For now, we'll do
     * the simplest thing, and simply clear out the entire pool. We may consider more sophisticated
     * approaches later. In other words, we ignore the memoryTrimType parameter
     *
     * @param memoryTrimType the kind of trimming we want to perform
     */
    public void trim(MemoryTrimType memoryTrimType) {
        trimToNothing();
    }

    /**
     * Allocates a new 'value' with the given size
     *
     * @param bucketedSize the logical size to allocate
     * @return a new value
     */
    protected abstract V alloc(int bucketedSize);

    /**
     * Frees the 'value'
     *
     * @param value the value to free
     */
    @VisibleForTesting
    protected abstract void free(V value);

    /**
     * Gets the bucketed size (typically something the same or larger than the requested size)
     *
     * @param requestSize the logical request size
     * @return the 'bucketed' size
     * @throws InvalidSizeException, if the size of the value doesn't match the pool's constraints
     */
    protected abstract int getBucketedSize(int requestSize);

    /**
     * Gets the bucketed size of the value
     *
     * @param value the value
     * @return bucketed size of the value
     * @throws InvalidSizeException,  if the size of the value doesn't match the pool's constraints
     * @throws InvalidValueException, if the value is invalid
     */
    protected abstract int getBucketedSizeForValue(V value);

    /**
     * Gets the size in bytes for the given bucketed size
     *
     * @param bucketedSize the bucketed size
     * @return size in bytes
     */
    protected abstract int getSizeInBytes(int bucketedSize);

    /**
     * The pool parameters may have changed. Subclasses can override this to update any state they
     * were maintaining
     */
    protected void onParamsChanged() {
    }

    /**
     * Determines if the supplied value is 'reusable'. This is called during {@link #release(Object)},
     * and determines if the value can be added to the freelists of the pool (for future reuse), or
     * must be released right away. Subclasses can override this to provide custom implementations
     *
     * @param value the value to test for reusability
     * @return true if the value is reusable
     */
    protected boolean isReusable(V value) {
        Preconditions.checkNotNull(value);
        return true;
    }

    /**
     * Ensure pool size invariants. The pool must either be below the soft-cap OR it must have no free
     * values left
     */
    private synchronized void ensurePoolSizeInvariant() {
        Preconditions.checkState(!isMaxSizeSoftCapExceeded() || mFree.mNumBytes == 0);
    }

    /**
     * Initialize the list of buckets. Get the bucket sizes (and bucket lengths) from the bucket sizes
     * provider
     *
     * @param inUseCounts map of current buckets and their in use counts
     */
    private synchronized void legacyInitBuckets(PlainIntArray inUseCounts) {
        Preconditions.checkNotNull(inUseCounts);

        // clear out all the buckets
        mBuckets.clear();

        // create the new buckets
        final PlainIntArray bucketSizes = mPoolParams.bucketSizes;
        if (bucketSizes != null) {
            for (int i = 0; i < bucketSizes.size(); ++i) {
                final int bucketSize = bucketSizes.keyAt(i);
                final int maxLength = bucketSizes.valueAt(i);
                int bucketInUseCount = inUseCounts.get(bucketSize, 0);
                mBuckets.put(
                        bucketSize,
                        new Bucket<V>(
                                getSizeInBytes(bucketSize),
                                maxLength,
                                bucketInUseCount,
                                mPoolParams.fixBucketsReinitialization));
            }
            mAllowNewBuckets = false;
        } else {
            mAllowNewBuckets = true;
        }
    }

    /**
     * Initialize the list of buckets. Get the bucket sizes (and bucket lengths) from the bucket sizes
     * provider
     */
    private synchronized void initBuckets() {
        final PlainIntArray bucketSizes = mPoolParams.bucketSizes;

        // create the buckets
        if (bucketSizes != null) {
            fillBuckets(bucketSizes);
            mAllowNewBuckets = false;
        } else {
            mAllowNewBuckets = true;
        }
    }

    /**
     * Clears and fills {@code mBuckets} with buckets
     *
     * @param bucketSizes bucket size to bucket's max length
     */
    private void fillBuckets(PlainIntArray bucketSizes) {
        mBuckets.clear();
        for (int i = 0; i < bucketSizes.size(); ++i) {
            final int bucketSize = bucketSizes.keyAt(i);
            final int maxLength = bucketSizes.valueAt(i);
            mBuckets.put(
                    bucketSize,
                    new Bucket<V>(
                            getSizeInBytes(bucketSize), maxLength, 0, mPoolParams.fixBucketsReinitialization));
        }
    }

    /**
     * Clears and fills {@code mBuckets} with buckets
     */
    private List<Bucket<V>> refillBuckets() {
        List<Bucket<V>> bucketsToTrim = new ArrayList<>(mBuckets.size());

        for (int i = 0, len = mBuckets.size(); i < len; ++i) {
            final Bucket<V> oldBucket = mBuckets.valueAt(i);
            final int bucketSize = oldBucket.mItemSize;
            final int maxLength = oldBucket.mMaxLength;
            final int bucketInUseCount = oldBucket.getInUseCount();
            if (oldBucket.getFreeListSize() > 0) {
                bucketsToTrim.add(oldBucket);
            }
            mBuckets.setValueAt(
                    i,
                    new Bucket<V>(
                            getSizeInBytes(bucketSize),
                            maxLength,
                            bucketInUseCount,
                            mPoolParams.fixBucketsReinitialization));
        }

        return bucketsToTrim;
    }

    /**
     * Gets rid of all free values in the pool At the end of this method, mFreeSpace will be zero
     * (reflecting that there are no more free values in the pool). mUsedSpace will however not be
     * reset, since that's a reflection of the values that were allocated via the pool, but are in use
     * elsewhere
     */
    @VisibleForTesting
    void trimToNothing() {
        final List<Bucket<V>> bucketsToTrim;

        synchronized (this) {
            if (mPoolParams.fixBucketsReinitialization) {
                bucketsToTrim = refillBuckets();
            } else {
                bucketsToTrim = new ArrayList<>(mBuckets.size());
                final PlainIntArray inUseCounts = new PlainIntArray();

                for (int i = 0; i < mBuckets.size(); ++i) {
                    final Bucket<V> bucket = mBuckets.valueAt(i);
                    if (bucket.getFreeListSize() > 0) {
                        bucketsToTrim.add(bucket);
                    }
                    inUseCounts.put(mBuckets.keyAt(i), bucket.getInUseCount());
                }

                legacyInitBuckets(inUseCounts);
            }

            // free up the stats
            mFree.reset();
            logStats();
        }

        // the pool parameters 'may' have changed.
        onParamsChanged();

        // Explicitly free all the values.
        // All the core data structures have now been reset. We no longer need to block other calls.
        // This is true even for a concurrent trim() call
        for (int i = 0; i < bucketsToTrim.size(); ++i) {
            final Bucket<V> bucket = bucketsToTrim.get(i);
            while (true) {
                // what happens if we run into an exception during the recycle. I'm going to ignore
                // these exceptions for now, and let the GC handle the rest of the to-be-recycled-bitmaps
                // in its usual fashion
                V item = bucket.pop();
                if (item == null) {
                    break;
                }
                free(item);
            }
        }
    }

    /**
     * Trim the (free portion of the) pool so that the pool size is at or below the soft cap. This
     * will try to free up values in the free portion of the pool, until (a) the pool size is now
     * below the soft cap configured OR (b) the free portion of the pool is empty
     */
    @VisibleForTesting
    synchronized void trimToSoftCap() {
        if (isMaxSizeSoftCapExceeded()) {
            trimToSize(mPoolParams.maxSizeSoftCap);
        }
    }

    /**
     * (Try to) trim the pool until its total space falls below the max size (soft cap). This will get
     * rid of values on the free list, until the free lists are empty, or we fall below the max size;
     * whichever comes first. NOTE: It is NOT an error if we have eliminated all the free values, but
     * the pool is still above its max size (soft cap)
     *
     * <p>The approach we take is to go from the smallest sized bucket down to the largest sized
     * bucket. This may seem a bit counter-intuitive, but the rationale is that allocating
     * larger-sized values is more expensive than the smaller-sized ones, so we want to keep them
     * around for a while.
     *
     * @param targetSize target size to trim to
     */
    @VisibleForTesting
    synchronized void trimToSize(int targetSize) {
        // find how much we need to free
        int bytesToFree = Math.min(mUsed.mNumBytes + mFree.mNumBytes - targetSize, mFree.mNumBytes);
        //LogUtil.error(TAG.getSimpleName(),"trimToSize B  mUsed.mNumBytes:"+mUsed.mNumBytes+"   mFree.mNumBytes:"+mFree.mNumBytes
         //+"   targetSize:"+targetSize+"   mFree.mNumBytes:"+mFree.mNumBytes);
        if (bytesToFree <= 0) {
            return;
        }
        if (FLog.isLoggable(FLog.VERBOSE)) {
            FLog.v(
                    TAG,
                    "trimToSize: TargetSize = %d; Initial Size = %d; Bytes to free = %d",
                    targetSize,
                    mUsed.mNumBytes + mFree.mNumBytes,
                    bytesToFree);
        }
        logStats();

        // now walk through the buckets from the smallest to the largest. Keep freeing things
        // until we've gotten to what we want
        for (int i = 0; i < mBuckets.size(); ++i) {
            if (bytesToFree <= 0) {
                break;
            }
            Bucket<V> bucket = mBuckets.valueAt(i);
            while (bytesToFree > 0) {
                V value = bucket.pop();
                if (value == null) {
                    break;
                }
                free(value);
                bytesToFree -= bucket.mItemSize;
                mFree.decrement(bucket.mItemSize);
            }
        }

        // dump stats at the end
        logStats();
        if (FLog.isLoggable(FLog.VERBOSE)) {
            FLog.v(
                    TAG,
                    "trimToSize: TargetSize = %d; Final Size = %d",
                    targetSize,
                    mUsed.mNumBytes + mFree.mNumBytes);
        }
    }

    /**
     * Gets the freelist for the specified bucket if it exists.
     *
     * @param bucketedSize the bucket size
     * @return the freelist for the bucket
     */
    private synchronized Bucket<V> getBucketIfPresent(int bucketedSize) {
        return mBuckets.get(bucketedSize, null);
    }

    /**
     * Gets the freelist for the specified bucket. Create the freelist if there isn't one
     *
     * @param bucketedSize the bucket size
     * @return the freelist for the bucket
     */
    @VisibleForTesting
    synchronized Bucket<V> getBucket(int bucketedSize) {
        // get an existing bucket
        Bucket<V> bucket = mBuckets.get(bucketedSize, null);
        if (bucket != null || !mAllowNewBuckets) {
            //LogUtil.error(TAG.getSimpleName() ,"reuse bucket:"+bucketedSize);
            return bucket;
        }

        // create a new bucket
        if (FLog.isLoggable(FLog.VERBOSE)) {
            FLog.v(TAG, "creating new bucket %s", bucketedSize);
        }
        //LogUtil.error(TAG.getSimpleName() ,"creating new bucket:"+bucketedSize);
        Bucket<V> newBucket = newBucket(bucketedSize);
        mBuckets.put(bucketedSize, newBucket);
        return newBucket;
    }

    Bucket<V> newBucket(int bucketedSize) {
        return new Bucket<V>(
                /*itemSize*/ getSizeInBytes(bucketedSize),
                /*maxLength*/ Integer.MAX_VALUE,
                /*inUseLength*/ 0,
                mPoolParams.fixBucketsReinitialization);
    }

    /**
     * Returns true if the pool size (sum of the used and the free portions) exceeds its 'max size'
     * soft cap as specified by the pool parameters.
     */
    @VisibleForTesting
    synchronized boolean isMaxSizeSoftCapExceeded() {
        final boolean isMaxSizeSoftCapExceeded =
                (mUsed.mNumBytes + mFree.mNumBytes) > mPoolParams.maxSizeSoftCap;
        if (isMaxSizeSoftCapExceeded) {
            mPoolStatsTracker.onSoftCapReached();
        }
        //LogUtil.error(TAG.getSimpleName(),"isMaxSizeSoftCapExceeded: "+isMaxSizeSoftCapExceeded);
        return isMaxSizeSoftCapExceeded;
    }

    /**
     * Can we allocate a value of size 'sizeInBytes' without exceeding the hard cap on the pool size?
     * If allocating this value will take the pool over the hard cap, we will first trim the pool down
     * to its soft cap, and then check again. If the current used bytes + this new value will take us
     * above the hard cap, then we return false immediately - there is no point freeing up anything.
     *
     * @param sizeInBytes the size (in bytes) of the value to allocate
     * @return true, if we can allocate this; false otherwise
     */
    @VisibleForTesting
    synchronized boolean canAllocate(int sizeInBytes) {
        //LogUtil.error(TAG.getSimpleName(),"canAllocate A mIgnoreHardCap:"+mIgnoreHardCap+"   sizeInBytes:"+sizeInBytes);
        if (mIgnoreHardCap) {
            //LogUtil.error(TAG.getSimpleName(),"canAllocate B");
            return true;
        }

        int hardCap = mPoolParams.maxSizeHardCap;
        //LogUtil.error(TAG.getSimpleName(),"canAllocate C hardCap:"+hardCap+ "    mUsed.mNumBytes:"+ mUsed.mNumBytes);

        // even with our best effort we cannot ensure hard cap limit.
        // Return immediately - no point in trimming any space
        if (sizeInBytes > hardCap - mUsed.mNumBytes) {
            //LogUtil.error(TAG.getSimpleName(),"canAllocate D onHardCapReached return false");
            mPoolStatsTracker.onHardCapReached();
            return false;
        }

        // trim if we need to
        int softCap = mPoolParams.maxSizeSoftCap;
        //LogUtil.error(TAG.getSimpleName(),"canAllocate E softCap:"+softCap);
        if (sizeInBytes > softCap - (mUsed.mNumBytes + mFree.mNumBytes)) {
            //LogUtil.error(TAG.getSimpleName(),"canAllocate F ");
            trimToSize(softCap - sizeInBytes);
        }

        //LogUtil.error(TAG.getSimpleName(),"canAllocate G");
        // check again to see if we're below the hard cap
        if (sizeInBytes > hardCap - (mUsed.mNumBytes + mFree.mNumBytes)) {
            //LogUtil.error(TAG.getSimpleName(),"canAllocate H onHardCapReached return false");
            mPoolStatsTracker.onHardCapReached();
            return false;
        }
        //LogUtil.error(TAG.getSimpleName(),"canAllocate I");

        return true;
    }

    /**
     * Simple 'debug' logging of stats. WARNING: The caller is responsible for synchronization
     */
    @SuppressLint("InvalidAccessToGuardedField")
    private void logStats() {
        if (FLog.isLoggable(FLog.VERBOSE)) {
            FLog.v(
                    TAG,
                    "Used = (%d, %d); Free = (%d, %d)",
                    mUsed.mCount,
                    mUsed.mNumBytes,
                    mFree.mCount,
                    mFree.mNumBytes);
        }
        /*
        LogUtil.error(TAG.getSimpleName(),String.format("region release Used = (%d, %d); Free = (%d, %d)",
                mUsed.mCount,
                mUsed.mNumBytes,
                mFree.mCount,
                mFree.mNumBytes));

         */
    }

    /**
     * Export memory stats regarding buckets used, memory caps, reused values.
     */
    public synchronized Map<String, Integer> getStats() {
        Map<String, Integer> stats = new HashMap<String, Integer>();
        for (int i = 0; i < mBuckets.size(); ++i) {
            final int bucketedSize = mBuckets.keyAt(i);
            final Bucket<V> bucket = mBuckets.valueAt(i);
            final String BUCKET_USED_KEY =
                    PoolStatsTracker.BUCKETS_USED_PREFIX + getSizeInBytes(bucketedSize);
            stats.put(BUCKET_USED_KEY, bucket.getInUseCount());
        }

        stats.put(PoolStatsTracker.SOFT_CAP, mPoolParams.maxSizeSoftCap);
        stats.put(PoolStatsTracker.HARD_CAP, mPoolParams.maxSizeHardCap);
        stats.put(PoolStatsTracker.USED_COUNT, mUsed.mCount);
        stats.put(PoolStatsTracker.USED_BYTES, mUsed.mNumBytes);
        stats.put(PoolStatsTracker.FREE_COUNT, mFree.mCount);
        stats.put(PoolStatsTracker.FREE_BYTES, mFree.mNumBytes);

        return stats;
    }

    /**
     * A simple 'counter' that keeps track of the number of items (mCount) as well as the byte mCount
     * for the number of items WARNING: this class is not synchronized - the caller must ensure the
     * appropriate synchronization
     */
    @NotThreadSafe
    @VisibleForTesting
    static class Counter {
        private static final String TAG = "com.facebook.imagepipeline.memory.BasePool.Counter";

        int mCount;
        int mNumBytes;

        /**
         * Add a new item to the counter
         *
         * @param numBytes size of the item in bytes
         */
        public void increment(int numBytes) {
            this.mCount++;
            this.mNumBytes += numBytes;
        }

        /**
         * 'Decrement' an item from the counter
         *
         * @param numBytes size of the item in bytes
         */
        public void decrement(int numBytes) {
            if (this.mNumBytes >= numBytes && this.mCount > 0) {
                this.mCount--;
                this.mNumBytes -= numBytes;
            } else {
                FLog.wtf(
                        TAG,
                        "Unexpected decrement of %d. Current numBytes = %d, count = %d",
                        numBytes,
                        this.mNumBytes,
                        this.mCount);
            }
        }

        /**
         * Reset the counter
         */
        public void reset() {
            this.mCount = 0;
            this.mNumBytes = 0;
        }
    }

    /**
     * An exception to indicate if the 'value' is invalid.
     */
    public static class InvalidValueException extends RuntimeException {
        public InvalidValueException(Object value) {
            super("Invalid value: " + value.toString());
        }
    }

    /**
     * An exception to indicate that the requested size was invalid
     */
    public static class InvalidSizeException extends RuntimeException {
        public InvalidSizeException(Object size) {
            super("Invalid size: " + size.toString());
        }
    }

    /**
     * A specific case of InvalidSizeException used to indicate that the requested size was too large
     */
    public static class SizeTooLargeException extends InvalidSizeException {
        public SizeTooLargeException(Object size) {
            super(size);
        }
    }

    /**
     * Indicates that the pool size will exceed the hard cap if we allocated a value of size
     * 'allocSize'
     */
    public static class PoolSizeViolationException extends RuntimeException {
        public PoolSizeViolationException(int hardCap, int usedBytes, int freeBytes, int allocSize) {
            super(
                    "Pool hard cap violation?"
                            + " Hard cap = "
                            + hardCap
                            + " Used size = "
                            + usedBytes
                            + " Free size = "
                            + freeBytes
                            + " Request size = "
                            + allocSize);
        }
    }
}
